package util.core;

import jama.Matrix;
import util.exceptions.UnbalancableEquationException;
import util.exceptions.UnknownElementException;

import java.util.ArrayList;


public class Equation {

    protected ArrayList<Molecule> left = new ArrayList<Molecule>();
    protected ArrayList<Molecule> right = new ArrayList<Molecule>();

    protected int[] balanceArray;

    public static Equation current;

    /**
     * Creates new Equation based on the compounds which are passed in
     *
     * @param left  Left side of the equation.
     * @param right Right side of the equation (order does not matter)
     * @throws UnknownElementException If there is a nonexistent element
     * @throws util.exceptions.UnbalancableEquationException
     *                                 If one element isn't on both sides
     */

    public Equation(ArrayList<String> left, ArrayList<String> right) throws UnknownElementException, UnbalancableEquationException {
        String[] compounds = new String[left.size()];
        String[] compounds2 = new String[right.size()];

        left.toArray(compounds);
        right.toArray(compounds2);
        initialize(compounds, true);
        initialize(compounds2, false);

        for (int i = 0; i < this.left.size(); i++) {
            Molecule c = this.left.get(i);
            if (contains(c, this.right)) {
                removeLeft(c, left);
                removeRight(c, right);
                i--;
            }
        }
    }

    public boolean contains(Molecule c, ArrayList<Molecule> list) {
        for (Molecule m : list) {
            if (c.equals(m)) {
                return true;
            }
        }
        return false;
    }

    private void ensureBalancable() throws UnbalancableEquationException {
        ArrayList<Atom> atoms = getAtoms();
        for (Atom j : atoms) {
            int countLeft = 0;
            int countRight = 0;

            for (Molecule c : left) {
                if (c.contains(j))
                    countLeft++;
            }
            for (Molecule c : right) {
                if (c.contains(j))
                    countRight++;
            }


            if (countRight == 0 || countLeft == 0) {
                throw new UnbalancableEquationException("At least one of the elements is not on both sides");
            }
        }
        if (left.size() == 0) {
            throw new UnbalancableEquationException("There is no reaction occurring.");
        }
    }

    private void removeRight(Molecule c, ArrayList<String> strings) {
        for (int i = 0; i < right.size(); i++) {
            if (right.get(i).equals(c)) {
                right.remove(i);
                strings.remove(i);
                i--;
            }
        }
    }

    private void removeLeft(Molecule c, ArrayList<String> strings) {
        for (int i = 0; i < left.size(); i++) {
            if (left.get(i).equals(c)) {
                left.remove(i);
                strings.remove(i);
                i--;
            }
        }
    }

    /**
     * Initializes the sides of the equation
     *
     * @param compounds The array of compound formulas
     * @param isLeft    The side of the equation to add the compounds to
     * @throws UnknownElementException If the element cannot be found
     */
    private void initialize(String[] compounds, boolean isLeft) throws UnknownElementException {
        for (String c : compounds) {
            if (isLeft)
                left.add(new Molecule(c.replaceAll(" ", "")));
            else
                right.add(new Molecule(c.replaceAll(" ", "")));
        }
    }

    /**
     * Balances the equation
     *
     * @return An array of integers which are the coefficients of the values from left to right
     * @throws UnbalancableEquationException When it cannot be balanced
     */
    public int[] balance() throws UnbalancableEquationException {
        ensureBalancable();
        balanceArray = new int[left.size() + right.size()];
        for (int i = 0; i < balanceArray.length; i++) {
            balanceArray[i] = 1;
        }
        ArrayList<Atom> atoms = getAtoms();
        double[][] solver, res;
        solver = new double[balanceArray.length - 1][balanceArray.length - 1];
        res = new double[balanceArray.length - 1][1];
        findUnique(atoms, solver, res);
        solve(solver, res);
        simplifyCoefficients();
        for (int val : balanceArray) {
            if (val < 0 && !ignoreNegatives())
                throw new UnbalancableEquationException();
        }
        verifyAtomCount();
        return balanceArray;
    }

    protected boolean ignoreNegatives() {
        return false;
    }

    private void verifyAtomCount() throws UnbalancableEquationException {
        ArrayList<Atom> atoms = getAtoms();
        for (Atom a : atoms) {
            int leftCount = count(left, a, 0);
            int rightCount = count(right, a, left.size());
            if (leftCount != rightCount)
                throw new UnbalancableEquationException("One of the sides is incorrect.");
        }
    }

    private int count(ArrayList<Molecule> list, Atom a, int offset) {
        int total = 0;
        int count = 0;
        for (Molecule molecule : list) {
            total += molecule.getCount(a) * balanceArray[offset + count];
            count++;
        }
        return total;
    }

    /**
     * Simplifies the coefficients by dividing them by their gcd
     *
     * @throws util.exceptions.UnbalancableEquationException
     *          e
     */
    private void simplifyCoefficients() throws UnbalancableEquationException {
        int gcd = balanceArray[0];
        try {
            for (int i = 1; i < balanceArray.length; i++) {
                gcd = gcd(gcd, balanceArray[i]);
            }
        } catch (ArithmeticException e) {
            throw new UnbalancableEquationException();
        }

        for (int i = 0; i < balanceArray.length; i++) {
            balanceArray[i] = balanceArray[i] / gcd;
        }
    }

    private void solve(double[][] solver, double[][] res) throws UnbalancableEquationException {
        Matrix a = new Matrix(solver);

        Matrix c;
        try {
            c = a.inverse().times(new Matrix(res)).times(a.det());
        } catch (Exception e) {
            throw new UnbalancableEquationException("There are multiple reactions occurring");
        }
        double b = a.det();
        double[][] k = c.getArray();

        for (int i = 0; i < k.length; i++) {
            int value = (int) Math.round(k[i][0]);
            balanceArray[i] = value;
        }
        balanceArray[balanceArray.length - 1] = (int) Math.round(b);

    }

    /**
     * Solves the equation
     *
     * @param atoms           The list of atoms.
     * @param finalSolver     The array of equations to be written to
     * @param finalResultants The array of resultants to be written to
     * @throws UnbalancableEquationException When it cannot be solved
     */
    private void findUnique(ArrayList<Atom> atoms, double[][] finalSolver, double[][] finalResultants) throws UnbalancableEquationException {
        double[][] solver, res;
        int totalMolecules = left.size() + right.size();
        int totalAtoms = atoms.size();

        solver = new double[totalAtoms + 1][totalMolecules - 1];
        res = new double[totalAtoms + 1][1];
        int row = 0;
        for (Atom b : atoms) {
            addEquation(b, solver, res, row);
            row++;
        }
        boolean allZero = addCharges(solver, res);
        int offSet = 0;
        if (allZero) {
            offSet = 1;
        }
        boolean[] unique = new boolean[totalAtoms + 1];
        ArrayList<SolveArray> sa = new ArrayList<SolveArray>();
        for (int i = offSet; i < totalAtoms + 1; i++) {
            SolveArray s = new SolveArray(solver[i], res[i][0]);
            if (!sa.contains(s)) {
                sa.add(s);
                unique[i] = true;
            }
        }
        int row2 = 0;
        for (int i = offSet; i < totalAtoms + 1 && row2 < totalMolecules - 1; i++) {
            if (unique[i]) {
                finalSolver[row2] = solver[i];
                finalResultants[row2] = res[i];
                row2++;
            }
        }
    }

    private boolean addCharges(double[][] solver, double[][] res) {
        int chargeCount = 0;
        for (int i = 0; i < left.size(); i++) {
            Molecule k = left.get(i);
            solver[0][i] = k.getCharge();
            if (k.getCharge() != 0)
                chargeCount++;
        }
        for (int i = 0; i < right.size(); i++) {
            Molecule k = right.get(i);
            if (k.getCharge() != 0)
                chargeCount++;
            if (i != right.size() - 1)
                solver[0][i + left.size()] = -k.getCharge();
            else
                res[0][0] = k.getCharge();
        }
        return chargeCount == 0;
    }

    /**
     * For each atom it adds the equation to solver
     *
     * @param currentElement The element to find an equation for
     * @param solver         A square array which is the solving array
     * @param res            A single column array which is a continuation of the solver
     * @param i              The index of row which is unused
     */
    private void addEquation(Atom currentElement, double[][] solver, double[][] res, int i) {
        for (int d = 0; d < left.size(); d++) {
            Molecule k = left.get(d);
            solver[i + 1][d] = k.getCount(currentElement);
        }
        for (int d = 0; d < right.size(); d++) {
            Molecule k = right.get(d);
            if (d != right.size() - 1)
                solver[i + 1][d + left.size()] = -k.getCount(currentElement);
            else
                res[i + 1][0] = k.getCount(currentElement);
        }
    }

    /**
     * Finds the greatest common divisor of p and q
     *
     * @param p The first number
     * @param q The second number
     * @return The gcd
     */
    public static int gcd(int p, int q) {
        if (q == 0) {
            return p;
        }
        return gcd(q, p % q);

    }

    /**
     * Gets the atoms which are contained in the equation
     *
     * @return An ArrayList of the atoms
     */
    private ArrayList<Atom> getAtoms() {
        ArrayList<Atom> atoms = new ArrayList<Atom>();
        for (Molecule c : left) {
            ArrayList<MoleculeComponent> j = c.getElements();
            for (MoleculeComponent g : j) {
                if (!atoms.contains(g.getElement())) {
                    atoms.add(g.getElement());
                }
            }
        }
        for (Molecule c : right) {
            ArrayList<MoleculeComponent> j = c.getElements();
            for (MoleculeComponent g : j) {
                if (!atoms.contains(g.getElement())) {
                    atoms.add(g.getElement());
                }
            }
        }
        return atoms;
    }

    @Override
    public String toString() {
        int count = 0;
        String representation = "";
        boolean isFirst = true;

        for (Molecule c : left) {
            if (!isFirst)
                representation += "+ ";
            if (balanceArray[count] != 1)
                representation += (balanceArray[count] + c.toString() + " ");
            else
                representation += (c + " ");
            isFirst = false;
            count++;
        }
        representation += "==> ";
        isFirst = true;

        for (Molecule c : right) {
            if (!isFirst)
                representation += "+ ";
            if (balanceArray[count] != 1)
                representation += (balanceArray[count] + c.toString() + " ");
            else
                representation += (c.toString() + " ");
            isFirst = false;
            count++;
        }
        return representation;
    }

    public ArrayList<Molecule> getLeft() {
        return left;
    }

    public ArrayList<Molecule> getRight() {
        return right;
    }

    public int[] getBalanceArray() {
        return balanceArray;
    }


    public int getIndexOf(Molecule target) {
        for (int i = 0; i < left.size(); i++)
            if (left.get(i).equals(target))
                return i;
        for (int i = 0; i < right.size(); i++)
            if (right.get(i).equals(target))
                return i + left.size();
        return -1;
    }
}
